SPB Git forge

spb/spb-cloud

Public
10commits 1branches 0releases
3.2 MBsize
maindefault branch
4 h agolast push
JavaScript 56.7% TypeScript 42.6%
5.9 KB · 132 lines typescript
Raw Blame History
1import { NextRequest, NextResponse } from "next/server";2import JSZip from "jszip";3import { prisma } from "@/lib/prisma";4import { loadFileData } from "@/lib/storage";5import { fileInShare, folderInShare, hasPasswordAccess, loadShare, shareInfo, shareStatusError, touchShare, type ShareWithTargets } from "@/lib/shareAccess";67function fileResponse(buffer: Buffer, name: string, mimeType: string, download: boolean) {8  return new NextResponse(new Uint8Array(buffer), {9    headers: {10      "Content-Type": download ? "application/octet-stream" : mimeType,11      "Content-Length": String(buffer.length),12      "Content-Disposition": `${download ? "attachment" : "inline"}; filename*=UTF-8''${encodeURIComponent(name)}`,13      "Cache-Control": "private, no-store",14    },15  });16}1718async function addFolderToZip(zip: JSZip, folderId: string, prefix: string) {19  const files = await prisma.file.findMany({ where: { folderId, deletedAt: null } });20  for (const file of files) {21    try {22      zip.file(prefix + file.name, await loadFileData(file.storagePath, file.isEncrypted));23    } catch { /* fichier illisible : ignoré */ }24  }25  const subfolders = await prisma.folder.findMany({ where: { parentId: folderId } });26  for (const sub of subfolders) await addFolderToZip(zip, sub.id, `${prefix}${sub.name}/`);27}2829/** Listing d'un dossier du partage : fichiers actifs, sous-dossiers, fil d'Ariane borné à la racine partagée. */30async function folderListing(share: ShareWithTargets, folderId: string) {31  const folder = await prisma.folder.findUnique({ where: { id: folderId }, select: { id: true, name: true, parentId: true } });32  if (!folder) return null;33  const [files, children] = await Promise.all([34    prisma.file.findMany({ where: { folderId, deletedAt: null }, orderBy: { name: "asc" }, select: { id: true, name: true, mimeType: true, size: true, updatedAt: true } }),35    prisma.folder.findMany({ where: { parentId: folderId }, orderBy: { name: "asc" }, select: { id: true, name: true, _count: { select: { files: true, children: true } } } }),36  ]);37  const crumbs: { id: string; name: string }[] = [];38  let cur: { id: string; name: string; parentId: string | null } | null = folder;39  while (cur) {40    crumbs.unshift({ id: cur.id, name: cur.name });41    if (cur.id === share.folder!.id) break;42    cur = cur.parentId ? await prisma.folder.findUnique({ where: { id: cur.parentId }, select: { id: true, name: true, parentId: true } }) : null;43  }44  return {45    folder: { id: folder.id, name: folder.name },46    crumbs,47    folders: children.map((c) => ({ id: c.id, name: c.name, files: c._count.files, children: c._count.children })),48    files: files.map((f) => ({ ...f, size: Number(f.size) })),49  };50}5152export async function GET(53  request: NextRequest,54  { params }: { params: { token: string } }55) {56  const share = await loadShare(params.token);57  const err = shareStatusError(share);58  if (err || !share) return err!;5960  const { searchParams } = new URL(request.url);61  const action = searchParams.get("action");62  const wantContent = searchParams.get("content") === "true";63  const fileId = searchParams.get("fileId");64  const folderId = searchParams.get("folderId");6566  // Mot de passe non validé (cookie signé absent) : on ne révèle que le minimum67  if (!hasPasswordAccess(request, share)) {68    const info = shareInfo(share);69    return NextResponse.json({70      requiresPassword: true,71      type: info.type,72      name: info.name,73      fileName: info.name, // compatibilité ancien client74      fileType: share.file?.mimeType ?? "inode/directory",75      mode: share.mode,76      expiresAt: info.expiresAt,77    });78  }7980  // ---- Partage de FICHIER ----81  if (share.file) {82    if (wantContent || action === "download") {83      const buffer = await loadFileData(share.file.storagePath, share.file.isEncrypted);84      const download = action === "download" || share.mode === "DOWNLOAD";85      await touchShare(share.id);86      return fileResponse(buffer, share.file.name, share.file.mimeType, download);87    }88    await touchShare(share.id);89    return NextResponse.json(shareInfo(share));90  }9192  // ---- Partage de DOSSIER ----93  const root = share.folder!;9495  if (fileId) {96    const f = await fileInShare(share, fileId);97    if (!f) return NextResponse.json({ error: "Fichier hors du partage" }, { status: 404 });98    if (wantContent || action === "download") {99      const buffer = await loadFileData(f.storagePath, f.isEncrypted);100      await touchShare(share.id);101      return fileResponse(buffer, f.name, f.mimeType, action === "download" || share.mode === "DOWNLOAD");102    }103    return NextResponse.json({ id: f.id, name: f.name, mimeType: f.mimeType, size: Number(f.size), mode: share.mode });104  }105106  // ZIP du dossier partagé (ou d'un sous-dossier)107  if (action === "download") {108    const target = folderId ?? root.id;109    if (!(await folderInShare(share, target))) return NextResponse.json({ error: "Dossier hors du partage" }, { status: 404 });110    const folder = await prisma.folder.findUnique({ where: { id: target }, select: { name: true } });111    const zip = new JSZip();112    await addFolderToZip(zip, target, "");113    const buffer = await zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE", compressionOptions: { level: 6 } });114    await touchShare(share.id);115    return new NextResponse(new Uint8Array(buffer), {116      headers: {117        "Content-Type": "application/zip",118        "Content-Disposition": `attachment; filename*=UTF-8''${encodeURIComponent((folder?.name || root.name) + ".zip")}`,119        "Cache-Control": "private, no-store",120      },121    });122  }123124  // Listing (racine du partage ou sous-dossier)125  const target = folderId ?? root.id;126  if (!(await folderInShare(share, target))) return NextResponse.json({ error: "Dossier hors du partage" }, { status: 404 });127  const listing = await folderListing(share, target);128  if (!listing) return NextResponse.json({ error: "Dossier introuvable" }, { status: 404 });129  await touchShare(share.id);130  return NextResponse.json({ ...shareInfo(share), ...listing });131}132